Skip to content

Build a single-file portable desktop app - #18

Open
owenpkent wants to merge 11 commits into
windows-updaterfrom
windows-portable
Open

owenpkent wants to merge 11 commits into
windows-updaterfrom
windows-portable

Conversation

@owenpkent

Copy link
Copy Markdown
Owner

Stacked on #13; merge that first, or retarget this to main after it lands.

What

Adds Offloader-{version}-portable.exe: the desktop app as one file that runs from any folder without installing. The existing ZIP stays; it is still the only portable way to get the CLI.

  • build/windows/portable_entry.py: new entry point that runs the GUI without the installation lock. gui_entry.py gains a lock flag; installed programs still require the lock.
  • offloader.spec: onefile target outside the bundle, with its own version resource.
  • build.py: checks the embedded version, signs and verifies the outer exe, records its digest in the build record (so --skip-build cannot reuse a swapped file), smoke-tests it, and lists it in SHA256SUMS.txt. --verify-only verifies its signature.
  • smoke.py --portable: starts a copy from an empty folder with a minimal PATH and fails if it writes anything beside itself.

Why no lock

No installer or maintenance helper manages the folder a portable exe sits in. Requiring the lock file would stop it starting; creating one would litter the user's folder. The installer never touches the portable exe, so there is nothing for the lock to protect.

Limits

  • No CLI (windowed exe) and no in-place update.
  • Unpacks to %TEMP% on each launch, so startup is slower than the installed app.
  • Only the outer exe is signed here. The native modules it unpacks keep the signatures they were frozen with, which may matter under strict app-control policies.

Testing

  • python -m pytest -q: 797 passed, 12 skipped.
  • python -m ruff check src tests scripts build/windows: clean.
  • build.py --clean --no-sign --no-installer in a Python 3.12 venv with requirements-build.txt: built, all smoke checks passed including the portable launch. Output 58,333,663 bytes.
  • Not run: signed build (needs the hardware token), NSIS installer build, visible desktop launch, clean-machine test.

The ZIP is portable only as a folder: Offloader.exe needs _internal beside it
and refuses to start without the installation lock file. This adds
Offloader-{version}-portable.exe, the desktop app as one self-extracting file
that runs from any folder.

It skips the installation lock. No installer or maintenance helper manages the
folder it sits in, so requiring a lock file would stop it starting and
creating one would litter the user's folder. The installed programs still take
the lock; the portable path goes through its own entry point rather than a
runtime guess about how the executable was frozen.

The builder treats it like the other release outputs: it signs and verifies
the outer executable, checks its embedded version, records its digest in the
build record so --skip-build cannot reuse a swapped file, and lists it in
SHA256SUMS.txt. The smoke runner starts a copy from an empty folder with a
minimal PATH and fails if it writes anything beside itself.

It has no CLI and no in-place update. The ZIP and installer still provide
those. The native modules it unpacks at launch keep the signatures they were
frozen with; only the outer executable is signed by this pipeline.

@owenpkent owenpkent left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed d458fcb for the portable entry point, PyInstaller one-file target, build record, signing order, output validation, and smoke runner.

No actionable defect found in the new portable-specific paths. The regular GUI retains its default installation lock, the portable entry opts out explicitly, the external executable's digest is included in the build record, and both reuse and verify-only paths check that artifact. Signing precedes the final record and output checksums. The smoke runner copies the executable into its own directory and checks for neighboring files after launch.

Validation: 23 focused artifact/entry/pipeline tests passed. The source-identity test was deselected because this review used an exported commit snapshot without Git metadata. Current-head CI reports success. A native Windows freeze, hardware-token signature, and interactive portable launch were not repeated on this Linux host.

@owenpkent owenpkent left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No additional actionable defect found in this incremental diff.

Reviewed the PyInstaller single-file target, portable entry behavior, installed-versus-portable lock handling, signing order, portable checksum record, verify-only checks and copied-executable smoke path. The separate portable target is reflected consistently in the build and artifact checks.

Validation: 22 focused portable artifact/entry/pipeline tests passed in a separate pinned source export; one symlink-dependent case skipped and one Git-source-identity test was deselected because the export lacks .git metadata. Current-head CI reports successful matrix/build/frozen checks; the diff check passes.

Limits: no fresh onefile freeze, physical clean-machine launch, hardware signing, antivirus exercise or actual installation. These are source-level checks plus recorded CI evidence, not a new portable-release qualification.

Pinned scope: declared base bf69fd36cd7923b6b09476963cb6d7250aa0fe7a, merge base bf69fd36cd7923b6b09476963cb6d7250aa0fe7a, head 12a4da481669ceac00243f91f62362f594bae012. Existing discussion and current-head checks were reviewed; no threads were resolved and no formal approval or merge is being issued.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant